<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="TwxYx" id="TwxYx"><span data-lake-id="u4621fbc6" id="u4621fbc6">典型回答</span></h1>
  <p data-lake-id="ubc355060" id="ubc355060"><br></p>
  <p data-lake-id="u8e975e94" id="u8e975e94"><span data-lake-id="u253e05ea" id="u253e05ea">CAS是一项乐观锁技术，是Compare And Swap的简称，顾名思义就是先比较再替换。CAS 操作包含三个操作数 —— 内存位置（V）、预期原值（A）和新值(B)。在进行并发修改的时候，会先比较A和V中取出的值是否相等，如果相等，则会把值替换成B，否则就不做任何操作。</span></p>
  <p data-lake-id="u1b0c7362" id="u1b0c7362"><span data-lake-id="uab39e2aa" id="uab39e2aa">​</span><br></p>
  <p data-lake-id="u25532428" id="u25532428"><span data-lake-id="u0ed379d2" id="u0ed379d2">当多个线程尝试使用CAS同时更新同一个变量时，只有其中一个线程能更新变量的值，而其它线程都失败，失败的线程并不会被挂起，而是被告知这次竞争中失败，并可以再次尝试。</span></p>
  <p data-lake-id="u1d0ef77b" id="u1d0ef77b"><br></p>
  <p data-lake-id="u5766482c" id="u5766482c"><span data-lake-id="uf96947e4" id="uf96947e4">在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于synchronized这种阻塞算法，CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。</span></p>
  <p data-lake-id="u348735f0" id="u348735f0"><span data-lake-id="ue97b2997" id="ue97b2997">​</span><br></p>
  <p data-lake-id="u41a5e627" id="u41a5e627"><span data-lake-id="ud92b2a9c" id="ud92b2a9c">CAS的主要应用就是实现乐观锁和锁自旋。</span></p>
  <h1 data-lake-id="llaxY" id="llaxY"><span data-lake-id="ua9a48f7e" id="ua9a48f7e">扩展知识</span></h1>
  <p data-lake-id="ud00148c3" id="ud00148c3"><br></p>
  <h2 data-lake-id="Ywfn9" id="Ywfn9"><span data-lake-id="u62cb7a26" id="u62cb7a26">ABA问题</span></h2>
  <p data-lake-id="u7d22fdf2" id="u7d22fdf2"><br></p>
  <p data-lake-id="ue2ae15d7" id="ue2ae15d7"><span data-lake-id="u8286acd9" id="u8286acd9">CAS会导致“ABA问题”。</span></p>
  <p data-lake-id="u14cb76ae" id="u14cb76ae"><span data-lake-id="u87a52aab" id="u87a52aab">​</span><br></p>
  <p data-lake-id="u3eaf0e1d" id="u3eaf0e1d"><span data-lake-id="u60ddfa69" id="u60ddfa69">CAS算法实现一个重要前提需要取出内存中某时刻的数据，而在下时刻比较并替换，那么在这个时间差类会导致数据的变化。</span></p>
  <p data-lake-id="ud8341ce3" id="ud8341ce3"><span data-lake-id="u5a7d659f" id="u5a7d659f">​</span><br></p>
  <p data-lake-id="u8d1a5d10" id="u8d1a5d10"><span data-lake-id="uecdcc59e" id="uecdcc59e">比如说一个线程1从内存位置V中取出A，这时候另一个线程2也从内存中取出A，并且2进行了一些操作变成了B，然后2又将V位置的数据变成A，这时候线程1进行CAS操作发现内存中仍然是A，然后1操作成功。尽管线程1的CAS操作成功，但是不代表这个过程就是没有问题的。</span></p>
  <p data-lake-id="u717aeb5d" id="u717aeb5d"><span data-lake-id="u0f6a1126" id="u0f6a1126">​</span><br></p>
  <p data-lake-id="u3c62ba12" id="u3c62ba12"><span data-lake-id="u76571a5c" id="u76571a5c">举个例子，线程1和线程2同时通过CAS尝试修改用户A余额，线程1和线程2同时查询当前余额为100元，然后线程2因为用户A要把钱借给用户B，先把余额从100改成50。然后又有用户C还给用户A 50元，线程2则又把50改成了100。这是线程1继续修改，把余额从100改成200。</span></p>
  <p data-lake-id="u866efb47" id="u866efb47"><span data-lake-id="ufb29b2c6" id="ufb29b2c6">​</span><br></p>
  <p data-lake-id="u66e5bddb" id="u66e5bddb"><span data-lake-id="ud7d4755c" id="ud7d4755c">虽然过程上金额都没问题，都改成功了，但是对于用户余额来说，丢失了两次修改的过程，在修改前用户C欠用户A 50元，但是修改后，用户C不欠钱了，而用户B欠用户A 50元了。而这个过程数据是很重要的。</span></p>
  <p data-lake-id="u441f33c1" id="u441f33c1"><span data-lake-id="u65c4f24a" id="u65c4f24a">​</span><br></p>
  <p data-lake-id="u027554d2" id="u027554d2"><span data-lake-id="u9dc66378" id="u9dc66378">部分乐观锁的实现是通过版本号（version）的方式来解决ABA问题，乐观锁每次在执行数据的修改操作时，都会带上一个版本号，一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作，否则就执行失败。因为每次操作的版本号都会随之增加，所以不会出现ABA问题，因为版本号只会增加不会减少。 </span></p>
  <p data-lake-id="uc85e255a" id="uc85e255a"><span data-lake-id="uae511eb4" id="uae511eb4">​</span><br></p>
  <p data-lake-id="u5fc05b44" id="u5fc05b44"><strong><span data-lake-id="ued263a76" id="ued263a76">在Java中，可以借助AtomicStampedReference</span></strong><span data-lake-id="u2d7bd331" id="u2d7bd331">，它是 Java 并发编程中的一个类，用于解决多线程环境下的“ABA”问题。AtomicStampedReference 通过同时维护一个引用和一个时间戳，可以解决 ABA 问题。它允许线程在执行 CAS 操作时，不仅检查引用是否发生了变化，还要检查时间戳是否发生了变化。这样，即使一个变量的值被修改后又改回原值，由于时间戳的存在，线程仍然可以检测到这中间的变化。</span></p>
  <p data-lake-id="u5944d685" id="u5944d685"><span data-lake-id="ue07d4b1e" id="ue07d4b1e">​</span><br></p>
  <pre lang="java"><code>
import java.util.concurrent.atomic.AtomicStampedReference;

public class Example {
    public static void main(String[] args) {
        String initialRef = "hollis";
        int initialStamp = 0;

        AtomicStampedReference&lt;String&gt; atomicStampedRef =
            new AtomicStampedReference&lt;&gt;(initialRef, initialStamp);

        String newRef = "hollis666";
        int newStamp = initialStamp + 1;

        boolean updated = atomicStampedRef.compareAndSet(initialRef, newRef, initialStamp, newStamp);
        System.out.println("Updated: " + updated);
    }
}

</code></pre>
  <p data-lake-id="u5663b345" id="u5663b345"><span data-lake-id="u94346fc6" id="u94346fc6">​</span><br></p>
  <p data-lake-id="ub0172ada" id="ub0172ada"><span data-lake-id="u88147a60" id="u88147a60">在这个例子中，AtomicStampedReference 初始化时不仅包含了一个初始引用 "hollis"，还包含了一个初始的时间戳 0。当我们尝试使用 compareAndSet 方法更新引用时，我们同时提供了期望的引用值和时间戳，以及新的引用值和时间戳。这样，只有当当前引用和时间戳都匹配期望值时，更新操作才会成功，从而避免了 ABA 问题。</span></p>
  <p data-lake-id="ufec0cdef" id="ufec0cdef"><span data-lake-id="uc5a2f695" id="uc5a2f695">​</span><br></p>
  <h2 data-lake-id="C3ufV" id="C3ufV"><span data-lake-id="u4596ccee" id="u4596ccee" style="color: rgb(0, 0, 0)">忙等待</span></h2>
  <p data-lake-id="u8f4f100c" id="u8f4f100c"><span data-lake-id="ueabfac87" id="ueabfac87">因为CAS基本都是要自旋的，这种情况下，如果并发冲突比较大的话，就会导致CAS一直在不断地重复执行，就会进入忙等待。</span></p>
  <p data-lake-id="ud39127aa" id="ud39127aa"><span data-lake-id="u7c2dc573" id="u7c2dc573">​</span><br></p>
  <p data-lake-id="uec04d3d9" id="uec04d3d9"><span data-lake-id="u7721ff1c" id="u7721ff1c">​</span><br></p>
  <blockquote data-lake-id="u5bb724e7" id="u5bb724e7">
   <p data-lake-id="u74d16b62" id="u74d16b62"><span data-lake-id="u2e309d1c" id="u2e309d1c" class="lake-fontsize-12" style="color: rgb(32, 33, 36)">忙等待</span><span data-lake-id="ub317aaa0" id="ub317aaa0">定义 一种进程执行状态。</span><span data-lake-id="u7e9d3094" id="u7e9d3094" class="lake-fontsize-12" style="color: rgb(32, 33, 36)"> 进程执行到一段循环程序的时候，由于循环判断条件不能满足而导致处理器反复循环，处于繁忙状态，该进程虽然繁忙但无法前进。</span></p>
  </blockquote>
  <p data-lake-id="u63c74ec5" id="u63c74ec5"><br></p>
  <p data-lake-id="u7c9596d6" id="u7c9596d6"><span data-lake-id="ua6771c63" id="ua6771c63">所以，一旦CAS进入忙等待状态一直执行不成功的话，会对CPU造成较大的执行开销。</span></p>
  <p data-lake-id="u73dd48f9" id="u73dd48f9"><br></p>
  <h2 data-lake-id="ykyEx" id="ykyEx"><span data-lake-id="u23af828d" id="u23af828d">乐观锁悲观锁</span></h2>
  <p data-lake-id="u25bf91ef" id="u25bf91ef"><br></p>
  <p data-lake-id="ufdf299f3" id="ufdf299f3"><span data-lake-id="u7042c6e1" id="u7042c6e1">乐观锁（ Optimistic Locking）其实是一种思想。乐观锁假设认为数据一般情况下不会造成冲突，所以在数据进行提交更新的时候，才会正式对数据的冲突与否进行检测，如果发现冲突了，则让返回用户错误的信息，让用户决定如何去做。</span></p>
  <p data-lake-id="ufd10cafc" id="ufd10cafc"><span data-lake-id="u098ea2a5" id="u098ea2a5">​</span><br></p>
  <p data-lake-id="ue33c57fc" id="ue33c57fc"><span data-lake-id="u055c54f5" id="u055c54f5">乐观锁的具体实现细节：主要就是两个步骤：冲突检测和数据更新。其实现方式有一种比较典型的就是CAS</span></p>
  <p data-lake-id="ub8a179d1" id="ub8a179d1"><span data-lake-id="uc718de00" id="uc718de00">​</span><br></p>
  <p data-lake-id="u7542c583" id="u7542c583"><span data-lake-id="u905d905c" id="u905d905c">相对于乐观锁，还有悲观锁，</span><span data-lake-id="u352ed0a0" id="u352ed0a0">这是一种对数据的修改抱有悲观态度的并发控制方式，一般在认为数据被并发修改的概率比较大的时候，需要在修改之前先加锁的时候使用。</span></p>
  <p data-lake-id="u928b24ce" id="u928b24ce"><span data-lake-id="udf1139ea" id="udf1139ea">​</span><br></p>
  <p data-lake-id="udf55c93a" id="udf55c93a"><span data-lake-id="u275ecbab" id="u275ecbab">JAVA中的synchronized就是一种悲观锁，但是这种悲观锁机制存在以下问题：</span></p>
  <p data-lake-id="ubfd9abaa" id="ubfd9abaa"><span data-lake-id="ube6a1b9e" id="ube6a1b9e">​</span><br></p>
  <ul list="u39861115">
   <li fid="uacf7b31b" data-lake-id="u6596b1fe" id="u6596b1fe"><span data-lake-id="ue4ac6400" id="ue4ac6400">在多线程竞争下，加锁、释放锁会导致比较多的上下文切换和调度延时，引起性能问题。</span></li>
   <li fid="uacf7b31b" data-lake-id="uf99ce85e" id="uf99ce85e"><span data-lake-id="uc4c5ca1c" id="uc4c5ca1c">一个线程持有锁会导致其它所有需要此锁的线程挂起。</span></li>
   <li fid="uacf7b31b" data-lake-id="u660d8b73" id="u660d8b73"><span data-lake-id="u700ce5b5" id="u700ce5b5">如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置，引起性能风险。</span></li>
  </ul>
  <p data-lake-id="u6f56a8b7" id="u6f56a8b7"><span data-lake-id="u58f32ec6" id="u58f32ec6">​</span><br></p>
  <h2 data-lake-id="qObry" id="qObry"><span data-lake-id="u2585451b" id="u2585451b" style="color: rgb(85, 85, 85)">Java对CAS的支持</span></h2>
  <p data-lake-id="u591dfdf5" id="u591dfdf5"><br></p>
  <p data-lake-id="u7ed77540" id="u7ed77540"><span data-lake-id="u429b9df3" id="u429b9df3">在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于synchronized这种阻塞算法，CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。</span></p>
  <p data-lake-id="ucbbdf919" id="ucbbdf919"><span data-lake-id="u3725801a" id="u3725801a">我们以java.util.concurrent中的AtomicInteger为例，看一下在不使用锁的情况下是如何保证线程安全的。主要理解getAndIncrement方法，该方法的作用相当于 i++ 操作。</span></p>
  <p data-lake-id="u8e39c2f4" id="u8e39c2f4"><br></p>
  <pre lang="java"><code>
public class AtomicInteger extends Number implements java.io.Serializable {  

        private volatile int value;  

    public final int get() {  
        return value;  
    }  

    public final int getAndIncrement() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return current;  
        }  
    }  

    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
}
</code></pre>
  <p data-lake-id="ud094cd85" id="ud094cd85"><br></p>
  <p data-lake-id="ue450450c" id="ue450450c"><span data-lake-id="ued3e045f" id="ued3e045f">在没有锁的机制下需要字段value要借助volatile原语，保证线程间的数据是可见的。这样在获取变量的值的时候才能直接读取。然后来看看++i是怎么做到的。</span></p>
  <p data-lake-id="u98085285" id="u98085285"><span data-lake-id="u28404bd0" id="u28404bd0">​</span><br></p>
  <p data-lake-id="u1d959daf" id="u1d959daf"><span data-lake-id="uc0a7af26" id="uc0a7af26">getAndIncrement采用了CAS操作，每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作，如果成功就返回结果，否则重试直到成功为止。而compareAndSet利用JNI来完成CPU指令的操作。</span></p>
  <p data-lake-id="u5862ac28" id="u5862ac28"><span data-lake-id="u0055bf0c" id="u0055bf0c">​</span><br></p>
  <h2 data-lake-id="ehDRH" id="ehDRH" style="text-align: justify"><span data-lake-id="u38853f1d" id="u38853f1d">CAS与对象创建</span></h2>
  <p data-lake-id="u8b6413b2" id="u8b6413b2"><br></p>
  <p data-lake-id="u7736bf4d" id="u7736bf4d"><span data-lake-id="u2114ed84" id="u2114ed84">另外，CAS还有一个应用，那就是在JVM创建对象的过程中。</span></p>
  <p data-lake-id="u76cff8ac" id="u76cff8ac"><span data-lake-id="u4d726681" id="u4d726681">​</span><br></p>
  <p data-lake-id="uc166a1eb" id="uc166a1eb"><span data-lake-id="u5334de84" id="u5334de84">对象创建在虚拟机中是非常频繁的。即使是仅仅修改一个指针所指向的位置，在并发情况下也不是线程安全的，可能正在给对象A分配内存空间，指针还没来得及修改，对象B又同时使用了原来的指针来分配内存的情况。</span></p>
  <p data-lake-id="udf74a0cf" id="udf74a0cf"><span data-lake-id="u343065c7" id="u343065c7">​</span><br></p>
  <p data-lake-id="ufc63c012" id="ufc63c012"><span data-lake-id="u3a3ae653" id="u3a3ae653">解决这个问题的方案有两种，其中一种就是采用CAS配上失败重试的方式保证更新操作的原子性。</span></p>
  <p data-lake-id="ubbd8c3be" id="ubbd8c3be"><span data-lake-id="ucb871c26" id="ucb871c26" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">​</span><br></p>
  <h2 data-lake-id="uxKNR" id="uxKNR"><span data-lake-id="u19462879" id="u19462879" style="color: rgb(51, 51, 51)">不使用synchronized如何实现线程安全的单例？</span></h2>
  <p data-lake-id="u2c233794" id="u2c233794"><br></p>
 </body>
</html>